﻿// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// Dual licensed under the MIT or GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// Date: May 5 2010 

#region

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Data.EntityClient;
using System.Linq;
using System.Web;
using System.Web.Script.Serialization;
using Salient.Web.DataAccess;
using Salient.Web.SQLite;
using Salient.Web.SQLite.Model;
using Salient.Web.Util;

#endregion

namespace Salient.Web.Security
{
    ///<summary>
    ///</summary>
    public class SQLiteSiteMapProvider : StaticSiteMapProvider
    {
        private const string STR_DuplicateNodeID = "Duplicate node ID";
        private const string STR_EmptyConnectionString = "Empty connection string";
        private const string STR_EmptyOrMissingConnectionStringName = "Empty or missing connectionStringName";
        private const string STR_InvalidParentID = "Invalid parent ID";
        private const string STR_InvalidSqlCacheDependency = "Invalid sqlCacheDependency";
        private const string STR_MissingConnectionString = "Missing connection string";
        private const string STR_MissingNodeID = "Missing node ID";
        private const string STR_MissingParentID = "Missing parent ID";


        private string _applicationName;
        private string _entityConnectionString;
        private object _lock = new object();
        private bool _rebuildSiteMap;
        private SiteMapNode _rootNode;

        ///<summary>
        ///</summary>
        public string EntityConnectionString
        {
            get { return _entityConnectionString; }
        }


        /// <summary>
        /// The name of the application using the custom membership provider.
        /// </summary>
        /// <returns>
        /// The name of the application using the custom membership provider.
        /// </returns>
        /// <remarks>
        /// The ApplicationName is used by the Roles class to associate users and roles with different applications. This enables multiple applications to use the same database to store user and role information without running into conflicts between duplicate user names or role names. Multiple ASP.NET applications can use the same database by specifying the same value in the ApplicationName property. The ApplicationName property can be set programmatically, or it can be set declaratively in the configuration file for the Web application using the applicationName attribute.
        ///
        /// If no ApplicationName property is specified, we recommend that the ApplicationVirtualPath property value be used.
        /// </remarks>
        public string ApplicationName
        {
            get
            {
                if (String.IsNullOrEmpty(_applicationName))
                {
                    _applicationName = ProviderUtility.GetDefaultAppName();
                }
                return _applicationName;
            }
            set
            {
                if (value != null && value.Length > 256)
                {
                    throw new ProviderException(SR.GetString(SR.PersonalizationProviderApplicationNameExceedMaxLength,
                                                             "256"));
                }
                _applicationName = value;
            }
        }

        /// <summary>
        /// Initializes the provider.
        /// </summary>
        /// <param name="name">
        /// The friendly name of the provider.
        /// </param>
        /// <param name="config">
        /// A collection of the name/value pairs representing the provider-specific attributes specified in the configuration 
        /// for this provider.
        /// </param>
        /// <exception cref="T:System.ArgumentNullException">
        /// The name of the provider is null.
        /// </exception>
        /// <exception cref="T:System.ArgumentException">
        /// The name of the provider has a length of zero.
        /// </exception>
        /// <exception cref="T:System.InvalidOperationException">
        /// An attempt is made to call 
        /// <see cref="M:System.Configuration.Provider.ProviderBase.Initialize(System.String,System.Collections.Specialized.NameValueCollection)" /> 
        /// on a provider after the provider has already been initialized.
        /// </exception>
        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            if (String.IsNullOrEmpty(name))
            {
                name = "SQLiteSiteMapProvider";
            }

            if (string.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", SR.GetString(SR.MembershipSqlProviderDescription));
            }

            base.Initialize(name, config);

            _applicationName = config["applicationName"];
            if (string.IsNullOrEmpty(_applicationName))
            {
                _applicationName = ProviderUtility.GetDefaultAppName();
            }

            if (_applicationName.Length > 256)
            {
                throw new ProviderException(SR.GetString(SR.ProviderApplicationNameTooLong));
            }

            _entityConnectionString = config["connectionStringName"];

            if (string.IsNullOrEmpty(_entityConnectionString))
            {
                throw new ProviderException(SR.GetString(SR.ConnectionNameNotSpecified));
            }

            SQLiteEntityConnectionHolder.GetConnection(_entityConnectionString);

            config.Remove("connectionStringName");
            config.Remove("applicationName");
            config.Remove("name");
            if (config.Count > 0)
            {
                string attribUnrecognized = config.GetKey(0);
                if (!String.IsNullOrEmpty(attribUnrecognized))
                    throw new ProviderException(SR.GetString(SR.ProviderUnrecognizedAttribute, attribUnrecognized));
            }

            using (
                SQLiteEntityConnectionHolder holder = SQLiteEntityConnectionHolder.GetConnection(_entityConnectionString)
                )
            {
                using (AspNetDbEntities ctx = new AspNetDbEntities(holder.Connection))
                {
                    GetOrCreateApplication(ctx);
                }
            }
        }


        /// <summary>
        /// Removes the specified <see cref="T:System.Web.SiteMapNode" /> object from the node collection that is maintained by 
        /// the site map provider.</summary>
        /// <param name="node">
        /// The <see cref="T:System.Web.SiteMapNode" /> to remove from the node collection maintained by the provider.</param>
        public void Remove(SiteMapNode node)
        {
            RemoveNode(node);
        }

        //2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
        /// <summary>
        /// Removes the specified <see cref="T:System.Web.SiteMapNode" /> object from the node collection that is maintained by 
        /// the site map provider.</summary>
        /// <param name="node">
        /// The <see cref="T:System.Web.SiteMapNode" /> to remove from the node collection maintained by the provider.</param>
        protected override void RemoveNode(SiteMapNode node)
        {
            using (
                SQLiteEntityConnectionHolder holder = SQLiteEntityConnectionHolder.GetConnection(_entityConnectionString)
                )
            {
                using (AspNetDbEntities ctx = new AspNetDbEntities(holder.Connection))
                {
                    Application app = GetOrCreateApplication(ctx);
                    using (EntityTransaction tx = holder.Connection.BeginTransaction())
                    {
                        try
                        {
                            SiteNode toRemove =
                                ctx.SiteNodes.FirstOrDefault(
                                    n => n.ApplicationId == app.ApplicationId && n.NodeId == node.Key);
                            if (toRemove == null)
                            {
                                throw new Exception("Node does not exist in data source");
                            }
                            //TODO: verify cascade here
                            ctx.DeleteObject(toRemove);
                            ctx.SaveChanges();
                            tx.Commit();
                        }
                        catch (Exception ex)
                        {
                            tx.Rollback();
                            this.LogError(ex);
                            throw new ProviderException(SR.ProviderUNKNOWNFailure);
                        }
                    }
                }
            }
            base.RemoveNode(node);
        }


        /// <summary>
        /// Retrieves a Boolean value indicating whether the specified <see cref="T:System.Web.SiteMapNode"/> object can be viewed by the user in the specified context.
        /// </summary>
        /// <returns>
        /// true if security trimming is enabled and <paramref name="node"/> can be viewed by the user or security trimming is not enabled; otherwise, false.
        /// </returns>
        /// <param name="context">The <see cref="T:System.Web.HttpContext"/> that contains user information.
        ///                 </param><param name="node">The <see cref="T:System.Web.SiteMapNode"/> that is requested by the user.
        ///                 </param><exception cref="T:System.ArgumentNullException"><paramref name="context"/> is null.
        ///                     - or -
        ///                 <paramref name="node"/> is null.
        ///                 </exception>
        public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)
        {
            if (HttpContext.Current == null)
            {
                return true;
            }


            bool isAccessibleToUser = base.IsAccessibleToUser(context, node);
            return isAccessibleToUser;
        }


        /// <summary>
        /// Adds a <see cref="T:System.Web.SiteMapNode" /> object to the node collection that is maintained by the site map 
        /// provider and specifies the parent <see cref="T:System.Web.SiteMapNode" /> object.
        /// NOTE: Add is not hierarchial - do not add graphs as they will not be represented in the database
        /// TODO: implement a custom static map to circumvent this and the duplicate url issue
        /// </summary>
        /// <param name="node">                     
        /// The <see cref="T:System.Web.SiteMapNode" /> to add to the node collection maintained by the provider.                 
        /// </param>
        /// <param name="parentNode">                     
        /// The <see cref="T:System.Web.SiteMapNode" /> that is the parent of <paramref name="node" />.                 
        /// </param>
        public void Add(SiteMapNode node, SiteMapNode parentNode)
        {
            AddNode(node, parentNode);
        }

        /// <summary>
        /// Adds a <see cref="T:System.Web.SiteMapNode" /> object to the node collection that is maintained by the site map 
        /// provider and specifies the parent <see cref="T:System.Web.SiteMapNode" /> object.
        /// NOTE: Add is not hierarchial - do not add graphs as they will not be represented in the database
        /// TODO: implement a custom static map to circumvent this and the duplicate url issue
        /// </summary>
        /// <param name="node">                     
        /// The <see cref="T:System.Web.SiteMapNode" /> to add to the node collection maintained by the provider.                 
        /// </param>
        /// <param name="parentNode">                     
        /// The <see cref="T:System.Web.SiteMapNode" /> that is the parent of <paramref name="node" />.                 
        /// </param>
        protected override void AddNode(SiteMapNode node, SiteMapNode parentNode)
        {
            using (
                SQLiteEntityConnectionHolder holder = SQLiteEntityConnectionHolder.GetConnection(_entityConnectionString)
                )
            {
                using (AspNetDbEntities ctx = new AspNetDbEntities(holder.Connection))
                {
                    Application app = GetOrCreateApplication(ctx);

                    using (EntityTransaction tx = holder.Connection.BeginTransaction())
                    {
                        try
                        {
                            SiteNode childEntity = GetChild(node, app);

                            if (parentNode != null)
                            {
                                SiteNode parent =
                                    ctx.SiteNodes.FirstOrDefault(
                                        n => n.ApplicationId == app.ApplicationId && n.NodeId == parentNode.Key);
                                if (parent == null)
                                {
                                    throw new Exception("ParentNode is not in datasource.");
                                }
                                parent.ChildNodes.Add(childEntity);
                            }
                            else
                            {
                                SiteNode parent =
                                    ctx.SiteNodes.FirstOrDefault(
                                        n => n.ApplicationId == app.ApplicationId && n.ParentNode == null);
                                if (parent != null)
                                {
                                    throw new Exception("ParentNode already in datasource.");
                                }

                                ctx.AddToSiteNodes(childEntity);
                            }

                            ctx.SaveChanges();
                            base.AddNode(node, parentNode);
                            tx.Commit();
                            _rebuildSiteMap = true;
                        }
                        catch (Exception ex)
                        {
                            tx.Rollback();
                            this.LogError(ex);
                            throw new ProviderException(SR.ProviderUNKNOWNFailure);
                        }
                    }
                }
            }
        }


        /// <summary>
        /// When overridden in a derived class, retrieves the root node of all the nodes that are currently managed 
        /// by the current provider. 
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.SiteMapNode"/> that represents the root node of the set of nodes that the 
        /// current provider manages. 
        /// </returns>
        protected override SiteMapNode GetRootNodeCore()
        {
            return BuildSiteMap();
        }

        /// <summary>
        /// testing method 
        /// TODO:move to a test wrapper
        /// </summary>
        public SiteMapNode Refresh()
        {
            _rebuildSiteMap = true;
            return BuildSiteMap();
        }

        /// <summary>
        /// testing method 
        /// TODO:move to a test wrapper
        /// </summary>
        public void ClearNodes()
        {
            base.Clear();
            _rootNode = null;
            _rebuildSiteMap = true;
        }

        /// <summary>
        /// When overridden in a derived class, loads the site map information from persistent storage and builds it in memory.
        /// </summary>
        /// <returns>
        /// The root <see cref="T:System.Web.SiteMapNode"/> of the site map navigation structure.
        /// </returns>
        public override SiteMapNode BuildSiteMap()
        {
            if (_rebuildSiteMap || _rootNode == null)
            {
                base.Clear();

                using (
                    SQLiteEntityConnectionHolder holder =
                        SQLiteEntityConnectionHolder.GetConnection(_entityConnectionString))
                {
                    using (AspNetDbEntities ctx = new AspNetDbEntities(holder.Connection))
                    {
                        Application app = GetOrCreateApplication(ctx);
                        try
                        {
                            List<SiteNode> nodes = ctx.SiteNodes
                                .Where(n => n.ApplicationId == app.ApplicationId).ToList();

                            SiteNode root = nodes
                                .FirstOrDefault(n => n.ParentNode == null);


                            _rootNode = BuildMap(root);
                            _rebuildSiteMap = false;
                        }
                        catch (Exception ex)
                        {
                            this.LogError(ex);
                            throw new ProviderException(SR.ProviderUNKNOWNFailure);
                        }
                    }
                }
            }

            return _rootNode;
        }


        private SiteMapNode BuildMap(SiteNode entityNode)
        {
            SiteMapNode siteMapNode = CreateNode(entityNode);
            base.AddNode(siteMapNode, null);
            BuildMap(entityNode, siteMapNode);
            return siteMapNode;
        }

        private void BuildMap(SiteNode entityNode, SiteMapNode siteMapNode)
        {
            foreach (SiteNode childEntityNode in entityNode.ChildNodes)
            {
                SiteMapNode childSiteMapNode = CreateNode(childEntityNode);
                base.AddNode(childSiteMapNode, siteMapNode);
                BuildMap(childEntityNode, childSiteMapNode);
            }
        }

        private SiteMapNode CreateNode(SiteNode childNode)
        {
            return CreateNode(childNode.NodeId, childNode.Title, childNode.Url, childNode.Description, childNode.Roles);
        }


        private SiteMapNode CreateNode(string key, string title, string url, string description, string roles)
        {
            return new SiteMapNode(this, key, url, title, description, GetRoles(roles), null, null, null);
        }


        private static string SetRoles(IList roles)
        {
            return new JavaScriptSerializer().Serialize(roles);
        }

        private static List<string> GetRoles(string rolesCsv)
        {
            if (string.IsNullOrEmpty(rolesCsv))
            {
                return new List<string>();
            }
            return new JavaScriptSerializer().Deserialize<List<string>>(rolesCsv);
        }

        private Application GetOrCreateApplication(AspNetDbEntities entities)
        {
            return ProviderUtility.GetOrCreateApplication(entities, ApplicationName);
        }

        private static SiteNode GetChild(SiteMapNode node, Application app)
        {
            string roles = null;
            if (node.Roles != null)
            {
                roles = SetRoles(node.Roles);
            }

            return new SiteNode
                {
                    NodeId = node.Key,
                    ApplicationId = app.ApplicationId,
                    Description = node.Description,
                    Roles = roles,
                    Title = node.Title,
                    Url = node.Url
                };
        }
    }
}

///// <summary>
///// code to add to a page to control access
///// TODO: add to salient access control
///// http://www.mytakeon.net/ishaihachlili/blogEngine/post/2006/09/The-SQL-SiteMap-Provider-and-trimming-by-roles-(originally-posted-2006-09-20).aspx
///// </summary>
//public void noid()
//{
//    if (User.Identity.IsAuthenticated)
//    {
//        bool isUserAllowed = false;
//        if (SiteMap.CurrentNode != null)
//        {
//            if (SiteMap.CurrentNode.Roles != null && SiteMap.CurrentNode.Roles.Count > 0)
//            {

//                foreach (string role in SiteMap.CurrentNode.Roles)
//                {
//                    if (context.User.IsInRole(role) || String.Equals(role, "*", StringComparison.InvariantCultureIgnoreCase))
//                    {
//                        isUserAllowed = true;
//                        break;
//                    }
//                }
//            }
//            else
//            {
//                //no roles for this page, deny access to the page
//                //if you want to allow access to pages without roles defined set to true
//                isUserAllowed = false;
//            }
//        }
//        if (!isUserAllowed)
//            Response.Redirect("AccessDenied.aspx");
//    }
//}